Skip to content

fix(rewrites): serve static files from public/ when rewrite target is a .html path#217

Open
yunus25jmi1 wants to merge 5 commits intocloudflare:mainfrom
yunus25jmi1:fix/issue-199-rewrites
Open

fix(rewrites): serve static files from public/ when rewrite target is a .html path#217
yunus25jmi1 wants to merge 5 commits intocloudflare:mainfrom
yunus25jmi1:fix/issue-199-rewrites

Conversation

@yunus25jmi1
Copy link
Contributor

Fixes #199.

Problem

When next.config rewrites() map a clean URL to a static .html file in public/ (a common pattern for serving pre-built static pages like /auth/no-access/auth/no-access.html), vinext was returning 404 because no Next.js route matches .html paths.

Root Cause

After afterFiles rewrites resolve a path to *.html, the routing logic in all three server paths would find no matching app/pages route for the .html URL and return 404, without ever checking the filesystem's public/ directory.

Fix

Three server paths updated:

  1. Pages Router dev (packages/vinext/src/index.ts): After afterFiles/fallback rewrites, before the final handler() call, check if the resolved URL points to a file in public/ and serve it directly.

  2. App Router dev (packages/vinext/src/server/app-dev-server.ts): The generated RSC entry now checks public/ for the rewritten pathname before rendering the 404 page. Added a new optional root parameter to generateRscEntry() so the public directory path is embedded in the generated virtual module.

  3. Production server (packages/vinext/src/server/prod-server.ts): After afterFiles rewrites and after fallback rewrites produce a path with a file extension, tryServeStatic() is called against the built clientDir (which contains public/ files) before passing to SSR.

Also added a staticMimeType() helper in index.ts to ensure correct Content-Type headers.

Tests

  • Integration tests in app-router.test.ts and pages-router.test.ts: GET /static-html-page (rewritten → /static-html-page.html) returns 200 with correct HTML content and text/html Content-Type.
  • Unit test in app-router.test.ts: generateRscEntry() embeds the public/ path in the generated code.
  • Fixture public/static-html-page.html files added to both app-basic and pages-basic fixtures; next.config rewrites updated.

What async rewrites() flat array does

Per Next.js semantics, when rewrites() returns a flat array, all rules go into afterFiles. vinext already handles this correctly via resolveNextConfig(). This PR fixes the serving of the rewritten destination when it's a static file.

@yunus25jmi1
Copy link
Contributor Author

@southpolesteve @elithrar kindly review the changes.

@yunus25jmi1
Copy link
Contributor Author

Kindly review the changes. @southpolesteve

@yunus25jmi1 yunus25jmi1 force-pushed the fix/issue-199-rewrites branch from 01837a4 to 098e14a Compare March 5, 2026 09:22
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@217

commit: 345c70c

@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this — the use case (rewriting clean URLs to static .html files in public/) is a real gap. The overall approach of checking the filesystem after rewrites fail to match a route is reasonable. However, there are several issues that should be addressed before merging.

Summary of issues

  1. Path traversal guard missing in dev server pathsprod-server.ts uses tryServeStatic() which has traversal protection, but the two dev-server paths (in index.ts and app-dev-server.ts) do not. While the rewrite destination is developer-controlled, cleanPathname in the RSC entry can be influenced by request URLs after middleware processing.

  2. MIME type map duplication — The MIME type mapping is now maintained in three separate places with inconsistent entries. This will drift over time.

  3. publicDir fallback is wrong for src/app layoutspath.dirname(appDir) produces /project/src/public when appDir is /project/src/app. The Vite root is always passed from the call site, but the fallback is misleading and would be silently wrong if ever hit.

  4. String concatenation instead of path.join in generated RSC code — The generated code uses publicDir + cleanPathname which doesn't normalize the path.

  5. deploy.ts (Cloudflare Workers Pages Router entry) not updated — Per AGENTS.md: "When fixing a bug in any of these files, check whether the same bug exists in the others." The Cloudflare Workers entry (deploy.ts) has the same afterFiles/fallback rewrite logic without static file serving. This is likely fine because Cloudflare's ASSETS binding serves static files at the platform level, but it should be explicitly acknowledged.

  6. Missing navigationContext cleanup in RSC entry — When returning the static file Response, setHeadersContext(null) and setNavigationContext(null) are not called, unlike all other early-return paths nearby.

const allowedOrigins = config?.allowedOrigins ?? [];
// Compute the public/ directory path for serving static files after rewrites.
// appDir is something like /project/app or /project/src/app; root is the Vite root.
const publicDir = path.join(root ?? path.dirname(appDir), "public").replace(/\\/g, "/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback path.dirname(appDir) is incorrect when appDir is /project/src/app — it would produce /project/src/public instead of /project/public. In practice root is always passed from the call site in index.ts, so this won't be hit at runtime. But the fallback is misleading. Consider throwing or logging a warning if root is not provided, rather than silently computing a wrong path.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 6, 2026

Review submitted with REQUEST_CHANGES. Here's a summary of the feedback:

Key issues identified:

  1. Path traversal guard missing — The two dev-server paths (index.ts and app-dev-server.ts) lack the traversal protection that tryServeStatic() in prod-server.ts already has. Suggested adding path.resolve() + prefix check.

  2. MIME type map duplicated 3xstaticMimeType() in index.ts, inline __mimeTypes in generated RSC code, and CONTENT_TYPES in prod-server.ts all have different subsets. Recommended extracting to a shared module.

  3. Wrong publicDir fallback for src/app projectspath.dirname(appDir) produces /project/src/public instead of /project/public when the app directory is under src/. The root parameter is always passed in practice, but the fallback is silently wrong.

  4. String concatenation in generated codepublicDir + cleanPathname doesn't normalize paths. Should use path.join() or add explicit traversal guard.

  5. Missing context cleanup — The static file return path in the RSC entry skips setHeadersContext(null) and setNavigationContext(null), unlike all other early-return paths.

  6. deploy.ts not updated — Noted that this is likely fine since Cloudflare's ASSETS binding handles static files, but should be explicitly acknowledged.

github run

@james-elicx
Copy link
Collaborator

I noticed in the issue that they were using a nested route, but we're only reproducing with a top-level route - it might be worth adding a nested one as well to ensure we're covering their scenario.

@yunus25jmi1
Copy link
Contributor Author

@james-elicx As I go through the issues, I'm going to working on it after dinner. Kindly review the remaining PR.

@yunus25jmi1 yunus25jmi1 force-pushed the fix/issue-199-rewrites branch from 098e14a to 8f8e18a Compare March 6, 2026 16:36
@yunus25jmi1
Copy link
Contributor Author

@southpolesteve @james-elicx The fixes is pushed. Kindly review the changes.

@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work on this PR — the use case is real (rewriting to static .html in public/) and the overall approach is sound. The shared MIME map is a nice improvement over the previous state. There are a few issues to address before merging.

Summary

  1. includes(".") in prod-server.ts is too broad — The afterFiles and fallback checks in prod-server.ts still use includes(".") instead of path.extname(), which will trigger unnecessary tryServeStatic filesystem lookups on URLs like /api/v2.0/data. The Pages Router dev server (index.ts) and the App Router dev server (generated RSC entry) both correctly use path.extname(). This should be consistent.

  2. Pages Router dev server only checks static files at the very end — In index.ts, the static file check runs after both afterFiles and fallback rewrites fail to match a route. But prod-server.ts correctly checks immediately after the afterFiles rewrite produces a .html path. This means in dev, if afterFiles rewrites /foo to /foo.html and there's no route match, the code tries fallback rewrites before checking the filesystem. This isn't a correctness bug (the test passes because fallback rewrites won't match either), but it's a behavioral inconsistency with prod and does an unnecessary fallback rewrite attempt. Consider checking right after afterFiles rewrites resolve, matching the prod-server pattern.

  3. publicDir fallback is misleading — The comment correctly notes that path.dirname(appDir) is wrong for src/app layouts, but the fallback root ?? path.resolve(appDir, "..") is still present. Since root is always passed from the call site, this won't be hit in practice, but it would be cleaner to either throw or use a more defensive default.

// If the rewritten path has a file extension, it may point to a static
// file in public/ (copied to clientDir during build). Try to serve it
// directly before falling through to SSR (which would return 404).
if (resolvedPathname.includes(".") && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includes(".") is too broad here — it triggers tryServeStatic on any URL containing a dot (e.g. /api/v2.0/data). Both index.ts and the generated RSC entry use path.extname() for this check. This should be consistent:

Suggested change
if (resolvedPathname.includes(".") && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) {
if (path.extname(resolvedPathname) && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) {

}
// Check if fallback targets a static file in public/
const fallbackPathname = fallbackRewrite.split("?")[0];
if (fallbackPathname.includes(".") && tryServeStatic(req, res, clientDir, fallbackPathname, compress)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same includes(".") issue as above:

Suggested change
if (fallbackPathname.includes(".") && tryServeStatic(req, res, clientDir, fallbackPathname, compress)) {
if (path.extname(fallbackPathname) && tryServeStatic(req, res, clientDir, fallbackPathname, compress)) {

// corresponds to a static file in the public/ directory (e.g., after a
// rewrite: /auth/no-access → /auth/no-access.html). Serve it directly so
// we don't hit the SSR 404 path.
const resolvedStaticPath = resolvedUrl.split("?")[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This static file check runs at the very end, after fallback rewrites have already been tried and failed. In contrast, prod-server.ts checks right after afterFiles rewrites resolve a .html path (line 901) and again after fallback rewrites (line 923). The dev server should match the prod behavior:

  1. Check for static file right after afterFiles rewrites resolve (before matchRoute)
  2. Check again after fallback rewrites resolve

This avoids unnecessary fallback rewrite attempts and keeps dev/prod parity — something AGENTS.md explicitly calls out as important.

// (e.g. /project/src/public instead of /project/public).
const publicDir = path.join(root ?? path.resolve(appDir, ".."), "public").replace(/\\/g, "/");
// Build import map for all page and layout files
const imports: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback root ?? path.resolve(appDir, "..") is documented as wrong for src/app layouts, but still present. Since root is always passed from index.ts, this code path won't execute in normal usage. But if someone adds a new call site without root, they'll get a silently wrong path. Consider making this defensive:

Suggested change
const imports: string[] = [];
const publicDir = path.join(root ?? (() => { console.warn("[vinext] generateRscEntry: root not provided, public/ path may be incorrect"); return path.resolve(appDir, ".."); })(), "public").replace(/\\/g, "/");

Or more simply, just assert that root is provided and throw if not.

if (__extname) {
const __publicRoot = ${JSON.stringify(publicDir)};
const __publicFilePath = __nodePath.resolve(__publicRoot, "." + cleanPathname);
// Path traversal guard — resolved path must stay inside public/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this traversal guard uses hardcoded "/" as the separator. On Windows (if anyone runs the dev server there), path.resolve returns backslash-separated paths, so startsWith(__publicRoot + "/") would fail. Consider using __nodePath.sep instead:

if (__publicFilePath.startsWith(__publicRoot + __nodePath.sep)) {

The index.ts version correctly uses path.sep. The generated RSC entry should match.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 6, 2026

Review submitted. Here's a summary of what I flagged:

Review Summary (REQUEST_CHANGES)

5 inline comments across 3 files:

  1. prod-server.ts (lines 901 & 923)includes(".") is too broad for the file extension check. URLs like /api/v2.0/data would trigger unnecessary tryServeStatic filesystem lookups. Both index.ts and the generated RSC entry correctly use path.extname()prod-server.ts should match. Suggested the fix for both locations.

  2. index.ts (line 2740) — Dev/prod parity issue. The Pages Router dev server only checks for static files after all rewrites (afterFiles + fallback) fail, while prod-server.ts checks immediately after afterFiles rewrites resolve. Per AGENTS.md, dev/prod server logic should stay in sync.

  3. app-dev-server.ts (line 68) — The publicDir fallback (root ?? path.resolve(appDir, "..")) is documented as wrong for src/app layouts but still present. Suggested either throwing or adding a warning if root isn't provided, rather than silently computing a wrong path.

  4. app-dev-server.ts (line 1710) — The traversal guard uses a hardcoded "/" separator. On Windows, path.resolve returns backslash-separated paths, so startsWith(__publicRoot + "/") would fail. The index.ts version correctly uses path.sep — the generated code should use __nodePath.sep instead.

github run

@yunus25jmi1 yunus25jmi1 force-pushed the fix/issue-199-rewrites branch from 8f8e18a to 35cebb9 Compare March 8, 2026 08:44
@james-elicx
Copy link
Collaborator

/bigbonk re-review please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Good work addressing the core issue — rewriting clean URLs to static .html files in public/ is a legitimate gap. The shared MIME map (mime.ts) is a nice consolidation that addresses the prior review's concern about duplication. The traversal guards are correctly implemented. Test coverage with both top-level and nested rewrites is solid — glad to see the nested case was added per @james-elicx's feedback.

However, there is one structural issue that should be addressed before merging, plus a couple of smaller items.

1. App Router dev/prod parity gap (important)

In app-dev-server.ts, the static file check only runs inside if (!match) (line 1793), meaning it fires after both afterFiles rewrites, matchRoute(), and fallback rewrites have all been attempted. But in prod-server.ts (lines 958, 980) and index.ts (lines 2984, 3029), the static file check runs immediately after each rewrite resolves, before route matching.

This means the App Router dev path:

  1. afterFiles rewrites /foo/foo.html
  2. matchRoute("/foo.html", routes) is called (always misses — no route matches .html)
  3. Fallback rewrites are attempted (unnecessary for a .html path)
  4. Then the !match block finally checks public/

While functionally correct today (tests pass because .html paths don't match routes and fallback rewrites won't match either), this is a behavioral inconsistency that AGENTS.md explicitly calls out as important to avoid. The static file check should be inserted right after afterFiles rewrites resolve (between current lines 1773-1775) and after fallback rewrites resolve (between lines 1788-1789).

2. __mimeTypes object re-created on every request

In the generated RSC entry, ${JSON.stringify(MIME_TYPES)} creates a new object on every request that enters the !match + has-extension branch. This should be hoisted to module scope in the generated code.

3. Nit: redundant path.resolve in index.ts

The path.resolve(root, "public") call is duplicated — once for resolvedPublicDir and again inside the traversal check. Minor, but easy to clean up.

What looks good

  • Shared MIME map in mime.ts — clean consolidation, addresses prior duplication concern
  • Traversal guardspath.resolve() + startsWith(dir + sep) pattern is correct in all three paths
  • Context cleanupsetHeadersContext(null) + setNavigationContext(null) is correctly called before the static file return in the RSC entry (addresses prior review item)
  • path.extname() usage — consistent across all three server paths (addresses prior review's includes(".") concern in prod-server)
  • Test coverage — both top-level and nested rewrites tested for both App Router and Pages Router
  • Nested rewrite test — addresses @james-elicx's request to cover the nested route scenario from the original issue

@@ -1781,6 +1791,27 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev/prod parity issue: This static file check only runs inside if (!match) — i.e. after afterFiles rewrites, matchRoute(), and fallback rewrites have all been attempted. In contrast, prod-server.ts (line 958) and index.ts (line 2984) check for static files immediately after afterFiles rewrites resolve, before route matching.

The current flow in the App Router dev path:

  1. afterFiles rewrites /foo/foo.html (line 1773)
  2. matchRoute("/foo.html", routes) — always misses (line 1777)
  3. Fallback rewrites attempted — unnecessary for .html (line 1780)
  4. !match block finally checks public/ (here)

The check should be inserted right after cleanPathname = __afterRewritten; (after line 1773), and again after cleanPathname = __fallbackRewritten; (after line 1788), matching the other server paths. This avoids unnecessary matchRoute and fallback rewrite calls and keeps dev/prod parity — something AGENTS.md explicitly calls out.

const publicDir = root ? path.join(root, "public") : null;
// Build import map for all page and layout files
const imports: string[] = [];
const importMap: Map<string, string> = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When root is not provided, publicDir becomes null and static file serving silently degrades. Since the only call site (index.ts line 2347) always passes root, this won't fire in practice. But the silent null makes it easy for a future caller to miss root and get no error.

Consider either making root required (since the single call site always provides it), or logging a warning when it's missing.

if (__stat.isFile()) {
const __content = __nodeFs.readFileSync(__publicFilePath);
const __ext = __extname.slice(1).toLowerCase();
const __mimeTypes = ${JSON.stringify(MIME_TYPES)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: ${JSON.stringify(MIME_TYPES)} creates a new object literal on every request that enters this branch. Since this is generated code inside _handleRequest(), the __mimeTypes constant should be hoisted to module scope in the generated code (near the other generated constants like routes, __configRewrites, etc.).

Comment on lines +2985 to +2987
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(root, "public", "." + afterFilesPathname);
if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: path.resolve(root, "public") is called twice — once for resolvedPublicDir and again inside the path.resolve(root, "public", ...) on the next line. You can reuse resolvedPublicDir:

Suggested change
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(root, "public", "." + afterFilesPathname);
if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) {
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname);
if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) {

Same pattern applies to the fallback block at lines 3030-3032.

// If the rewritten path has a file extension, it may point to a static
// file in public/ (copied to clientDir during build). Try to serve it
// directly before falling through to SSR (which would return 404).
if (path.extname(resolvedPathname) && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good — using path.extname() here is correct and consistent with the other server paths. The prior review flagged includes(".") as too broad; glad to see this was addressed.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully with REQUEST_CHANGES. Here's a summary of what I flagged:

Key issue:

  • App Router dev/prod parity gap — In app-dev-server.ts, the static file check only runs inside if (!match) after all rewrites and route matching have been exhausted. In prod-server.ts and index.ts, the check runs immediately after each rewrite resolves (before route matching). This is a behavioral inconsistency that AGENTS.md explicitly calls out as important to avoid. The fix is to move the static file check right after afterFiles/fallback rewrites set cleanPathname, matching the other server paths.

Smaller items:

  • __mimeTypes object is re-created on every request in the generated RSC entry — should be hoisted to module scope
  • publicDir silently becomes null when root isn't provided — consider making it required or warning
  • Redundant path.resolve(root, "public") calls in index.ts

What's good:

  • Shared MIME map in mime.ts addresses prior duplication concern
  • Traversal guards are correct in all paths
  • Context cleanup (setHeadersContext(null) / setNavigationContext(null)) is properly handled
  • path.extname() used consistently (fixes prior includes(".") issue)
  • Test coverage for both top-level and nested rewrites across both routers

github run

@yunus25jmi1 yunus25jmi1 force-pushed the fix/issue-199-rewrites branch from 35cebb9 to 424fe43 Compare March 8, 2026 09:17
@yunus25jmi1
Copy link
Contributor Author

@elithrar @james-elicx @southpolesteve conflict resolved. Kindly review the PR and it's changes.

@james-elicx
Copy link
Collaborator

For what it's worth, you don't need to keep tagging us
/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR addresses a real gap — rewriting clean URLs to static .html files in public/ is a legitimate use case and the 404 behavior was a bug. The shared MIME map in mime.ts is a good consolidation, traversal guards are correctly implemented, context cleanup is handled, and the test coverage (both top-level and nested rewrites, across both routers + a unit test for the generated entry) is solid.

However, there is one structural issue that's been flagged in all three prior automated reviews but remains unaddressed, plus a couple of smaller items.

Issues

  1. App Router dev/prod parity gap (important) — The static file check in app-dev-server.ts only runs inside if (!match) at line 1837, meaning it fires after afterFiles rewrites, matchRoute(), and fallback rewrites have all been attempted. In contrast, index.ts (lines 2984, 3029) and prod-server.ts (lines 958, 980) check for static files immediately after each rewrite resolves, before route matching. This is functionally correct today but is a behavioral inconsistency that AGENTS.md explicitly calls out as important to avoid.

  2. __mimeTypes object re-created on every request — In the generated RSC entry, the ${JSON.stringify(MIME_TYPES)} expression at line 1851 creates a new object literal on every request that enters the !match + has-extension branch. It should be hoisted to module scope in the generated code.

  3. Redundant path.resolve(root, "public") calls — In index.ts, path.resolve(root, "public") is computed for resolvedPublicDir and then again in the path.resolve(root, "public", ...) call on the next line. The resolvedPublicDir variable should be reused. Same pattern at lines 3030-3032.

  4. publicDir silently becomes null — When root is not provided to generateRscEntry(), publicDir is null and static file serving silently degrades. The only call site always provides root, but the silent null makes it easy for a future caller to miss. Consider making root required or logging a warning.

}
}

if (!match) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev/prod parity issue — This is the third review cycle flagging this, and it remains the most important structural issue in the PR.

This static file check only runs inside if (!match), i.e. after afterFiles rewrites → matchRoute() → fallback rewrites have all been attempted. But in index.ts (line 2984) and prod-server.ts (line 958), the check runs immediately after afterFiles rewrites resolve, before route matching.

Concrete flow difference in the App Router dev path:

  1. afterFiles rewrites /foo/foo.html (line 1811)
  2. matchRoute("/foo.html", routes) is called (line 1821) — always misses
  3. Fallback rewrites attempted (line 1824) — unnecessary for a .html path
  4. !match block finally checks public/ (here, line 1837)

The fix is to insert a static file check right after cleanPathname = __afterRewritten; (after line 1817) and again after cleanPathname = __fallbackRewritten; (after line 1832), matching the other two server paths. This avoids unnecessary matchRoute and fallback rewrite calls.

Per AGENTS.md: "When fixing a bug in any of these files, check whether the same bug exists in the others. Do not leave known bugs as 'follow-ups' — fix them in the same PR." The parity requirement applies equally to behavioral consistency across the three server implementations.

if (__stat.isFile()) {
const __content = __nodeFs.readFileSync(__publicFilePath);
const __ext = __extname.slice(1).toLowerCase();
const __mimeTypes = ${JSON.stringify(MIME_TYPES)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ${JSON.stringify(MIME_TYPES)} expression creates a new object literal on every request that enters this code path. Since this is generated code inside _handleRequest(), the constant should be hoisted to module scope in the generated code (near the other generated constants like routes, __configRewrites, etc.).

Something like:

// At module scope in the generated code
const __mimeTypes = { ... };

Then reference __mimeTypes here instead of creating a new object each time.

// appDir is something like /project/app or /project/src/app; root is the Vite root.
// We require `root` for correctness — path.dirname(appDir) is wrong for src/app layouts
// (e.g. /project/src/public instead of /project/public).
const publicDir = root ? path.join(root, "public") : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When root is not provided, publicDir becomes null and static file serving silently degrades (the generated code has if (__extname && null !== null) which is always false). The only call site (index.ts line 2347) always passes root, so this won't fire in practice. But the silent degradation makes it easy for a future caller to miss root and get no error or warning.

Consider either:

  • Making root required (since the single call site always provides it), or
  • Logging a warning when it's missing, e.g.:
if (!root) console.warn("[vinext] generateRscEntry: root not provided, static file serving after rewrites will be disabled");

const afterFilesPathname = afterRewrite.split("?")[0];
if (path.extname(afterFilesPathname)) {
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(root, "public", "." + afterFilesPathname);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path.resolve(root, "public") is computed for resolvedPublicDir on the line above, then path.resolve(root, "public", ...) is called again here instead of reusing resolvedPublicDir. Minor but easy to clean up:

Suggested change
const publicFilePath = path.resolve(root, "public", "." + afterFilesPathname);
const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname);

Same pattern applies to the fallback block at line 3031.

const fallbackPathname = fallbackRewrite.split("?")[0];
if (path.extname(fallbackPathname)) {
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(root, "public", "." + fallbackPathname);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same redundant path.resolve(root, "public") — reuse resolvedPublicDir here too:

Suggested change
const publicFilePath = path.resolve(root, "public", "." + fallbackPathname);
const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname);

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted with REQUEST_CHANGES. Here's what I flagged:

Key issue:

  • App Router dev/prod parity gap — The static file check in app-dev-server.ts only runs inside if (!match) after all rewrites and route matching have been exhausted. In prod-server.ts and index.ts, the check runs immediately after each rewrite resolves (before route matching). This has been flagged in all three prior review cycles and remains unaddressed. AGENTS.md explicitly requires keeping these server paths in sync.

Smaller items:

  • __mimeTypes object is re-created on every request in the generated RSC entry — should be hoisted to module scope
  • Redundant path.resolve(root, "public") calls in index.ts (computed twice instead of reusing the variable)
  • publicDir silently becomes null when root isn't provided — consider making it required or warning

What looks good:

  • Shared MIME map in mime.ts — clean consolidation
  • Traversal guards correctly implemented in all paths
  • Context cleanup (setHeadersContext(null) / setNavigationContext(null)) properly handled
  • path.extname() used consistently
  • Test coverage for both top-level and nested rewrites across both routers

github run

…flare#199)

Address code review feedback:
- Extract shared MIME type map (server/mime.ts) eliminating 3x duplication
- Add path traversal guard using resolve + startsWith pattern
- Use path.extname() instead of string splitting
- Use path.join/resolve in generated RSC entry instead of string concat
- Clean up navigation/headers context before returning static response
- Add nested route test (public/auth/no-access.html via rewrite)
@yunus25jmi1 yunus25jmi1 force-pushed the fix/issue-199-rewrites branch from 424fe43 to 506d825 Compare March 8, 2026 10:42
@james-elicx
Copy link
Collaborator

/bigbonk re-review this pr

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

The fix is well-targeted — rewriting clean URLs to static .html files in public/ is a real use case (issue #199) and the 404 was a legitimate bug. The overall approach is sound: after rewrites resolve to a path with a file extension, check the filesystem before falling through to route matching / SSR.

Good things in this PR:

  • Shared MIME map (mime.ts) — clean consolidation that replaces the old CONTENT_TYPES in prod-server and avoids drift
  • Traversal guardspath.resolve() + startsWith(dir + sep) pattern is correct and consistent in all three server paths
  • Context cleanupsetHeadersContext(null) + setNavigationContext(null) called before returning in the generated RSC entry
  • path.extname() used consistently — avoids the overly broad includes(".") that was in the old prod-server code
  • Dev/prod parity — all three server paths (index.ts, app-dev-server.ts, prod-server.ts) now check for static files immediately after each rewrite resolves, before route matching. I note the automated reviewer kept flagging a parity issue, but the current code is actually correct: in app-dev-server.ts, the afterFiles static check (line 1824) runs before matchRoute (line 1844), and the fallback static check (line 1857) runs before the fallback matchRoute (line 1874).
  • Test coverage — both top-level and nested rewrites tested across both routers, plus a unit test for the generated entry
  • deploy.ts correctly left unchanged — Cloudflare's ASSETS binding serves static files at the platform level

I have a few items flagged inline — mostly minor, but one is a subtle correctness concern worth addressing.

const afterFilesPathname = afterRewrite.split("?")[0];
if (path.extname(afterFilesPathname)) {
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: resolvedPublicDir is computed on line 2985, but then path.resolve(resolvedPublicDir, "." + afterFilesPathname) re-derives the same base. This is fine, but the "." + afterFilesPathname pattern relies on afterFilesPathname starting with / (which it does, since it comes from afterRewrite.split("?")[0] which is a rewrite destination). Worth a brief comment or assertion to document this assumption, since if a rewrite destination ever lacked a leading slash, the path resolution would be wrong.

Same applies to the fallback block at line 3031.

// Check if fallback targets a static file in public/
const fallbackPathname = fallbackRewrite.split("?")[0];
if (path.extname(fallbackPathname)) {
const resolvedPublicDir = path.resolve(root, "public");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: path.resolve(root, "public") is computed again here identically to the afterFiles block above (line 2985). Since both blocks are in the same function scope, you could hoist resolvedPublicDir above both blocks to avoid redundant computation on every request that hits fallback rewrites.

Suggested change
const resolvedPublicDir = path.resolve(root, "public");
const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname);

(with resolvedPublicDir hoisted before the afterFiles block)

if (!root) {
console.warn("[vinext] generateRscEntry: root not provided, static file serving after rewrites will be disabled");
}
const publicDir = root ? path.join(root, "public") : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good handling — the warning when root is missing is a clear improvement over the previous silent wrong fallback. The null propagation through the generated code (${JSON.stringify(null)} !== null → always false) cleanly disables the feature.

One thought: since root is effectively required for this feature to work and there's only one call site that always provides it, you could consider making it required (drop the ?) and let TypeScript enforce it. But the current approach is also fine for forward compatibility.

Comment on lines +1826 to +1827
const __afterPublicRoot = ${JSON.stringify(publicDir)};
const __afterPublicFile = __nodePath.resolve(__afterPublicRoot, "." + cleanPathname);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle issue: ${JSON.stringify(publicDir)} is evaluated at code-generation time and embedded as a string literal in the generated module. This means the value is the publicDir at the time generateRscEntry() was called. If someone moves or symlinks the public/ directory after the dev server starts, the embedded path would be stale. This is fine in practice (dev servers restart on config changes), but it's worth noting.

More importantly: the __nodePath.resolve(__afterPublicRoot, "." + cleanPathname) call assumes cleanPathname starts with /. Since it comes from __afterRewritten (a rewrite destination), this should always be true for well-formed Next.js config. But a malformed rewrite destination like foo.html (no leading slash) would resolve relative to the current working directory rather than public/, potentially reading files outside the project. The traversal guard on line 1828 would catch this (the resolved path wouldn't start with __afterPublicRoot), so it's not a security issue, but worth a brief comment.

setNavigationContext(null);
return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } });
}
} catch { /* file doesn't exist or not readable */ }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bare catch {} silently swallows all errors, including permission errors (EACCES), too-many-open-files (EMFILE), etc. For a dev server, it would be helpful to log unexpected errors:

Suggested change
} catch { /* file doesn't exist or not readable */ }
} catch (e) { if (e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', e); }

Same applies to the fallback block's catch at line 1871.

// If the rewritten path has a file extension, it may point to a static
// file in public/ (copied to clientDir during build). Try to serve it
// directly before falling through to SSR (which would return 404).
if (path.extname(resolvedPathname) && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good — using path.extname() here is correct and consistent with the other server paths. Clean improvement over the old includes(".") approach.

export const MIME_TYPES: Record<string, string> = {
html: "text/html; charset=utf-8",
htm: "text/html; charset=utf-8",
css: "text/css",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice consolidation. One missing entry that's fairly common in Next.js apps: wasm: "application/wasm". Cloudflare Workers apps in particular use .wasm files. Not blocking, but worth adding if you're touching this file anyway.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of what I flagged:

Overall assessment: This PR is in good shape. The fix correctly addresses issue #199 and the implementation is solid across all three server paths.

Key finding: The automated reviewer's persistent complaint about dev/prod parity in app-dev-server.ts is no longer valid — the current code correctly checks for static files immediately after each rewrite resolves, before route matching (afterFiles check at line 1824 → before matchRoute at line 1844; fallback check at line 1857 → before fallback matchRoute at line 1874).

Inline comments posted (7 total):

  1. index.ts:2986 — The "." + pathname pattern assumes a leading / from rewrite destinations. Worth documenting the assumption.
  2. index.ts:3030 — Redundant path.resolve(root, "public") — could reuse resolvedPublicDir from the afterFiles block.
  3. app-dev-server.ts:70 — The root warning is good; could consider making root required since the only call site always provides it.
  4. app-dev-server.ts:1826-1827 — Note about the embedded path being static and the "." + cleanPathname assumption (traversal guard catches edge cases, so not a security issue).
  5. app-dev-server.ts:1838 — Bare catch {} silently swallows all errors including permission/resource errors. Suggested logging non-ENOENT errors for dev server diagnostics.
  6. prod-server.ts:958 — Positive note: path.extname() usage is correct and consistent.
  7. mime.ts:12 — Suggested adding wasm: "application/wasm" since it's common in Cloudflare Workers apps.

github run

@yunus25jmi1
Copy link
Contributor Author

Merge the PR to main. The PR is in a good state.

@james-elicx
Copy link
Collaborator

Merge the PR to main. The PR is in a good state.

Looks like Bonk has a few more comments before it's happy to approve

@yunus25jmi1
Copy link
Contributor Author

Merge the PR to main. The PR is in a good state.

Looks like Bonk has a few more comments before it's happy to approve

Yeah, I observed this.

# Conflicts:
#	packages/vinext/src/index.ts
#	packages/vinext/src/server/app-dev-server.ts
…add wasm MIME type, document leading-slash assumption
@yunus25jmi1
Copy link
Contributor Author

Addressed all bonk review items:

  1. Hoisted resolvedPublicDir in index.ts — computed once before both afterFiles and fallback blocks instead of redundantly in each
  2. Replaced bare catch {} in app-dev-server.ts — both afterFiles and fallback static file checks now log non-ENOENT errors
  3. Added wasm: "application/wasm" to mime.ts — relevant for Cloudflare Workers apps
  4. Documented leading-slash assumption — added comments in both app-dev-server.ts and index.ts explaining that "." + pathname relies on rewrite destinations starting with /, and that the traversal guard catches malformed paths regardless
  5. Merged upstream/main — resolved conflicts, kept both bodySizeLimit (from feat: support experimental.serverActions.bodySizeLimit from next.config #338) and root parameter

Re: making root required — kept it optional for forward compatibility as bonk noted is also fine. The warning + null propagation cleanly disables the feature when missing.

@james-elicx
Copy link
Collaborator

/bigbonk please re-review this pr

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This PR fixes a real gap — rewriting clean URLs to static .html files in public/ is a common Next.js pattern and the 404 was a legitimate bug. The approach is sound: after rewrites resolve to a path with a file extension, check the filesystem before falling through to route matching.

What looks good

  • Shared MIME map (mime.ts) — clean consolidation that replaces the old duplicated CONTENT_TYPES in prod-server and prevents drift
  • Traversal guardspath.resolve() + startsWith(dir + sep) pattern is correctly implemented in all three server paths
  • Context cleanupsetHeadersContext(null) + setNavigationContext(null) properly called before returning the static file response in the generated RSC entry
  • path.extname() used consistently across all three server paths
  • Dev/prod parity — all three server paths check for static files immediately after each rewrite resolves, before route matching. The afterFiles check fires before matchRoute in all three paths, and the fallback check fires before fallback matchRoute in both app-dev-server and index.ts
  • Test coverage — both top-level and nested rewrites tested across both routers, plus a unit test for the generated entry code
  • Non-ENOENT error logging in catch blocks — good for dev server diagnostics

Items

I have a few inline comments — one suggestion, a couple of observations, and a testing gap worth noting. Nothing blocking.

const publicDir = root ? path.join(root, "public") : null;
// Build import map for all page and layout files
const imports: string[] = [];
const importMap: Map<string, string> = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The publicDir is null when root isn't provided, and the generated code bakes in ${JSON.stringify(null)} !== null which is always false — effectively disabling the feature silently. The warning on lines 70-71 is good, but since the only call site (index.ts:2384) always passes root, consider making root required (drop the ?) so TypeScript enforces this at the call site. If a future caller forgets root, they'd get a type error instead of silently broken static file serving.

Non-blocking — the current code is safe.

const __configRewrites = ${JSON.stringify(rewrites)};
const __configHeaders = ${JSON.stringify(headers)};
const __allowedOrigins = ${JSON.stringify(allowedOrigins)};
const __mimeTypes = ${JSON.stringify(MIME_TYPES)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good — __mimeTypes is correctly hoisted to module scope in the generated code. This was flagged in earlier reviews and has been properly addressed.

Comment on lines 1921 to 1941
if (__afterExtname && ${JSON.stringify(publicDir)} !== null) {
const __afterPublicRoot = ${JSON.stringify(publicDir)};
// "." + cleanPathname works because rewrite destinations always start with "/";
// the traversal guard below catches any malformed path regardless.
const __afterPublicFile = __nodePath.resolve(__afterPublicRoot, "." + cleanPathname);
if (__afterPublicFile.startsWith(__afterPublicRoot + __nodePath.sep)) {
try {
const __afterStat = __nodeFs.statSync(__afterPublicFile);
if (__afterStat.isFile()) {
const __afterContent = __nodeFs.readFileSync(__afterPublicFile);
const __afterExt = __afterExtname.slice(1).toLowerCase();
setHeadersContext(null);
setNavigationContext(null);
return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } });
}
} catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); }
}
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static file check is correctly positioned — it runs immediately after cleanPathname = __afterRewritten (line 1917) and before matchRoute (which comes after this block). This matches the positioning in index.ts and prod-server.ts. The automated reviewer's persistent complaint about this being inside if (!match) was incorrect for the current code.

One observation: __afterPublicRoot on line 1922 re-evaluates ${JSON.stringify(publicDir)} which is a static string literal, so the JS engine will inline it. But it could be even cleaner to reference a single module-scope constant (like __mimeTypes) rather than embedding the same JSON.stringify(publicDir) literal in both the afterFiles and fallback blocks. Minor style nit.


it("applies custom headers from next.config.js on API routes", async () => {
const res = await fetch(`${baseUrl}/api/hello`);
expect(res.headers.get("x-custom-header")).toBe("vinext-app");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test coverage for both top-level and nested rewrites. One gap worth noting: there are no tests for fallback rewrites targeting static files. The code adds static file checking after both afterFiles and fallback rewrites, but only afterFiles is tested (the rewrite rules in the fixture config are all in afterFiles). Consider adding a fallback rewrite rule that maps to a .html file and a corresponding test — this would exercise the fallback static file path in all three servers.

Non-blocking since afterFiles is the common case from the original issue.

// If the rewritten path has a file extension, it may point to a static
// file in public/ (copied to clientDir during build). Try to serve it
// directly before falling through to SSR (which would return 404).
if (path.extname(resolvedPathname) && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean improvement — path.extname() is more precise than the old includes('.') and is now consistent across all three server paths. The tryServeStatic() function already has its own traversal guard, so delegating to it here is the right call.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

The review was submitted successfully. Here's a summary of what I posted:

Review submitted on PR #217 with 5 inline comments:

  1. app-dev-server.ts:76 — Suggestion to make root required instead of optional, since the only call site always provides it and the null fallback silently disables the feature.

  2. app-dev-server.ts:1082 — Positive note that __mimeTypes is correctly hoisted to module scope (addressing prior review feedback).

  3. app-dev-server.ts:1921-1941 — Confirmed the static file check is correctly positioned before matchRoute, correcting the automated reviewer's persistent (and now incorrect) complaint about dev/prod parity. Minor style nit about repeated JSON.stringify(publicDir) literals.

  4. tests/app-router.test.ts:2050 — Noted a gap: fallback rewrites targeting static files are untested. Only afterFiles rewrites are covered. Non-blocking.

  5. prod-server.ts:958 — Positive note on the clean improvement from includes('.') to path.extname().

Overall assessment: the PR is in good shape. The core fix is correct, the three server paths are properly synchronized, and the prior review items have been addressed.

github run

@yunus25jmi1
Copy link
Contributor Author

Addressed latest bonk review items:

  1. Hoisted __publicDir to module scope in generated RSC entry — replaces repeated JSON.stringify(publicDir) inline literals with a single const __publicDir at module scope (next to __mimeTypes, __configRewrites, etc.)
  2. Added fallback rewrite static file tests — new test in both app-router.test.ts and pages-router.test.ts that exercises the fallback rewrite path targeting a .html file in public/. Added corresponding fixture config entries and public/fallback-page.html files.
  3. Updated unit testgenerateRscEntry unit test now verifies __publicDir is hoisted to module scope in the generated code.

Re root required vs optional: kept optional for now since bonk said "current approach is also fine for forward compatibility" and it makes the function more defensive for future callers.

All 213 app-router tests and 130/132 pages-router tests pass (2 pre-existing failures unrelated to this PR: "hydration proxy script" and "client bundle chunks").

@james-elicx
Copy link
Collaborator

/bigbonk re-review the pr please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This PR correctly addresses issue #199 — rewriting clean URLs to static .html files in public/ is a common Next.js pattern and the 404 was a real bug. The approach is sound and well-structured across all three server paths.

What looks good

  • Shared MIME map in mime.ts — clean consolidation that eliminates prior duplication
  • Traversal guards are correctly implemented using path.resolve() + startsWith(dir + sep) in all three paths
  • Context cleanup (setHeadersContext(null) / setNavigationContext(null)) before returning in the RSC entry
  • path.extname() used consistently across all server paths
  • Dev/prod parity — the static file check runs immediately after each rewrite resolves, before route matching, in all three server paths
  • Test coverage — top-level, nested, and fallback rewrites tested for both App Router and Pages Router, plus a unit test for the generated entry code
  • Non-ENOENT error logging in catch blocks — helpful for dev server diagnostics

Issues

I have two inline comments — one correctness concern and one code quality nit. Neither is blocking, but the first is worth addressing.

// appDir is something like /project/app or /project/src/app; root is the Vite root.
// We need `root` for correctness — path.dirname(appDir) is wrong for src/app layouts
// (e.g. /project/src/public instead of /project/public).
const publicDir = root ? path.join(root, "public") : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor inconsistency: index.ts (line 3069) uses path.resolve(root, "public") while this uses path.join(root, "public"). Since this value is embedded as a static string in the generated code and then compared against __nodePath.resolve(...) results in the traversal guard (line 1923), path.resolve would be more defensively correct — it guarantees a fully normalized absolute path, whereas path.join doesn't normalize . or .. segments.

In practice this is fine because root comes from Vite's config.root which is always an absolute resolved path. But for consistency with index.ts and to make the traversal guard comparison more robust:

Suggested change
const publicDir = root ? path.join(root, "public") : null;
const publicDir = root ? path.resolve(root, "public") : null;

trailingSlash?: boolean,
config?: AppRouterConfig,
instrumentationPath?: string | null,
root?: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function now has 10 positional parameters, which makes the call site at index.ts:2377-2384 hard to read (you have to count arguments to know what maps to what). Not something to fix in this PR, but worth noting as tech debt — a future refactor could bundle the later optional params into an options object.

For this PR specifically: since the only call site always provides root, making it required (dropping the ?) would be a stronger API choice. The null fallback + warning is safe but it means TypeScript won't catch a missing root at a future call site. Up to you — the current approach works.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of what I posted:

Overall assessment: The PR is in good shape. The fix correctly addresses issue #199 across all three server paths with proper security (traversal guards), cleanup (context nulling), and test coverage (top-level, nested, and fallback rewrites for both routers).

Two inline comments:

  1. app-dev-server.ts:70path.join vs path.resolve inconsistency. index.ts uses path.resolve(root, "public") while app-dev-server.ts uses path.join(root, "public"). Since the value is compared against __nodePath.resolve(...) results in the traversal guard, path.resolve would be more defensively correct. Suggested the fix.

  2. app-dev-server.ts:57 — The function now has 10 positional parameters, making call sites hard to read. Not something to fix in this PR, but noted as tech debt. Also noted that making root required (dropping ?) would be a stronger API choice since the only call site always provides it.

github run

Address bonk review: path.join → path.resolve for publicDir computation
in app-dev-server.ts, matching the pattern used in index.ts. path.resolve
guarantees a fully normalized absolute path, making the traversal guard
comparison more robust.
@yunus25jmi1
Copy link
Contributor Author

yunus25jmi1 commented Mar 8, 2026

Addressed the Bonk review item:

  • path.joinpath.resolve for publicDir in app-dev-server.ts — now consistent with index.ts.
    path.resolve guarantees a fully normalized absolute path, making the traversal guard comparison more robust. The unit test has been updated accordingly.

Re: the 10 positional parameters — acknowledged as technical debt, but not addressed in this PR as Bonk noted.

All 213 app-router tests pass. Typecheck clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for rewrites

2 participants